/*
 * MIT License
 *
 * Copyright (c) 2019 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package cn.seaboot.commons.file;

import cn.seaboot.commons.digest.Hex;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.*;

/**
 * 此工具类，1 个函数对应 1 个功能，简单的图片处理，都可以完成。
 * <p>
 * 可以看出来，下列代码中，每一个函数都重新创建 Graphics2D，实际上，你不需要在每个操作都重新创建 1 个 Graphics2D。
 * 举个例子：你可能需要添加水印、添加文字、然后缩放大小，创建 1 个 Graphics2D 即可完成全部操作，你可以参考下列代码，构建一个针对业务的工具类。
 *
 * @author Mr.css
 * @version 2020-08-24
 */
public class BufferedImageUtil {

    public static final String IMAGE_PNG = "png";
    public static final String IMAGE_JPG = "jpg";


    private BufferedImageUtil() {
    }


    /**
     * 读取文件的前几个字节来判断图片格式，
     * 注意：很多输入流是一次性使用的，使用的时候需要用到临时文件，这个函数并没有想象的实用。
     * <p>
     * better method：{@link BufferedImage#getType()}, this method can get RGB-type。
     * 更好的做法，是通过图片 rgb 类型进行判断，这需要在业务代码中处理。
     *
     * @param is 输入流
     * @return jpg/png/gif/bmp/null
     * @see BufferedImage#getType() better method
     */
    public static String getImageType(InputStream is) throws IOException {
        byte[] b = new byte[4];
        if (is.read(b, 0, b.length) > 0) {
            String type = Hex.encodeToString(b).toUpperCase();
            if (type.contains("FFD8FF")) {
                return "jpg";
            } else if (type.contains("89504E47")) {
                return "png";
            } else if (type.contains("47494638")) {
                return "gif";
            } else if (type.contains("424D")) {
                return "bmp";
            }
        }
        return null;
    }

    /**
     * BufferedImage 转换为输入流，此函数会将整个文件读入内存，注意内存大小。
     * 给 word、excel 压水印的时候可能会用到。
     *
     * @param src        图片
     * @param formatName 图片类型，E.G.: png
     * @throws IOException 数据格式化异常，无法转换为目标类型的图片
     */
    public InputStream toInputStream(BufferedImage src, String formatName) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ImageIO.write(src, formatName, os);
        return new ByteArrayInputStream(os.toByteArray());
    }

    /**
     * 背景色替换
     *
     * @param src   图片
     * @param color 图片
     * @return 图片
     */
    public static BufferedImage setBackgroundColor(BufferedImage src, Color color) {
        BufferedImage ret = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = ret.createGraphics();
        graphics.drawImage(src, 0, 0, color, null);
        graphics.dispose();
        return ret;
    }

    /**
     * 生成文字图标
     *
     * @param text       字符串
     * @param background 颜色 E.G.: Color.BLACK，默认透明背景
     * @param font       字体 E.G.: new Font("黑体", Font.PLAIN, 48)
     * @param width      宽度
     * @param height     高度
     */
    public static BufferedImage createFontIcon(String text, Color background, Font font, int width, int height) {
        BufferedImage ret = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = ret.createGraphics();
        // 设置画布大小
        g.setClip(0, 0, width, height);

        // 绘制底色
        if (background != null) {
            g.setColor(background);
            g.fillRect(0, 0, width, height);
        }

        // 设置画笔颜色
        g.setColor(Color.WHITE);
        g.setFont(font);
        // 启用抗锯齿
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        // 文字指示工具，用于计算文字宽度
        FontMetrics fm = g.getFontMetrics(font);
        int y = (height - font.getSize()) / 2 + fm.getAscent() - fm.getDescent();
        int x = (width - fm.stringWidth(text)) / 2;
        g.drawString(text, x, y);
        g.dispose();
        return ret;
    }


    /**
     * 图片缩小
     * <p>
     * 将图片等比例缩小之后，使之刚好放在一个矩形范围内
     *
     * @param src    资源图像
     * @param width  矩形宽
     * @param height 矩形高
     * @return -
     * @throws IOException -
     */
    public static BufferedImage scale(BufferedImage src, int width, int height) throws IOException {
        int h = src.getHeight(), w = src.getWidth();

        if (w > width) {
            h *= (double) width / w;
            w = width;
        }

        if (h > height) {
            w *= (double) height / h;
            h = height;
        }

        if (h < height || w < width) {
            Image scaledImage = src.getScaledInstance(w, h, BufferedImage.SCALE_SMOOTH);
            BufferedImage outputImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            outputImage.getGraphics().drawImage(scaledImage, 0, 0, null);
            return outputImage;
        } else {
            // 如果大小没变化，则直接使用原图
            return src;
        }
    }

    /**
     * 图片缩放处理
     * <p>
     * 将图片等比例缩小之后，使之刚好放在一个矩形范围内。
     * <p>
     * Tips：实际操作过程中，图片缩小之后，可能还要进行一些 2D 操作，这个函数可以提供代码参考。
     *
     * @param src    资源图像
     * @param width  预期宽度
     * @param height 预期高度
     * @return BufferedImage
     */
    public static BufferedImage scaleBy2D(BufferedImage src, int width, int height) {
        int h = src.getHeight(), w = src.getWidth();

        if (w > width) {
            h *= (double) width / w;
            w = width;
        }

        if (h > height) {
            w *= (double) height / h;
            h = height;
        }

        if (h < height || w < width) {

            BufferedImage ret = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = ret.createGraphics();
            // 抗锯齿
            // g.setRenderingHint(RenderingHints.KEY_ANTIALIASING , RenderingHints.VALUE_ANTIALIAS_ON);
            // 插值算法，质量稍高
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            // 插值算法，质量中等
            // g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawImage(src, 0, 0, width, height, null);
            g.dispose();
            return ret;
        } else {
            return src;
        }
    }

    /***
     * 质量压缩，仅限于 jpg 图片
     *
     * <pre>
     *   Some guidelines:
     *     0.75 high quality
     *     0.5  medium quality
     *     0.25 low quality
     * </pre>
     *
     *  <p>
     *  JPG 与 PNG 的区别（想要进一步压缩图片大小，可以考虑转为 jpg）：
     *  <p>
     *  1、JPG 格式是一种有损压缩格式，而 PNG 是无损压缩算法的位图格式。
     *  <p>
     *  2、由于 JPG 是有损格式，将图像压缩在很小的储存空间，一定程度上会造成图像数据的损伤，会造成锯齿状边缘。
     *  而 PNG 可以为原图像定义 256 个透明层次，使图片边缘平滑融合，从而消除图片锯齿边缘。
     *  <p>
     *  3、在使用中 jpg 不能作为透明图片使用，而 PNG 支持透明效果，可以作为背景透明的图片使用。
     *
     * @param quality   压缩质量 0.0-1.0 setting of desired quality level.
     * @param src        源文件
     * @param os        目标文件路径
     * @throws IOException io
     */
    public static void compress(BufferedImage src, OutputStream os, float quality) throws IOException {
        try (ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
            ImageWriter writer;
            if (src.getType() == BufferedImage.TYPE_4BYTE_ABGR) {
                writer = ImageIO.getImageWritersByFormatName("png").next();
//                writer = new PNGImageWriter(new PNGImageWriterSpi());
            } else {
                writer = ImageIO.getImageWritersByFormatName("jpg").next();
            }

            writer.setOutput(ios);
            ImageWriteParam param = writer.getDefaultWriteParam();
            // 设置压缩模式为 MODE_EXPLICIT，表示要显式地设置压缩参数,通过 setCompressionQuality() 方法设置压缩质量
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(quality);
            writer.write(null, new IIOImage(src, null, null), param);
            writer.dispose();
        }
    }

    /**
     * 图片剪切
     *
     * @param src    目标图像
     * @param left   起始X坐标
     * @param top    起始Y坐标
     * @param width  裁剪宽度
     * @param height 裁剪高度
     * @return BufferedImage
     */
    public static BufferedImage clip(BufferedImage src, int left, int top, int width, int height) {
        BufferedImage newImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = newImg.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(src, 0, 0, width, height, left, top, left + width, top + height, null);
        g.dispose();
        return newImg;
    }

    /**
     * 图片旋转
     *
     * @param src    目标图像
     * @param degree 旋转角度
     * @return BufferedImage
     */
    public static BufferedImage rotate(BufferedImage src, int degree) {
        int w = src.getWidth();
        int h = src.getHeight();
        int type = src.getColorModel().getTransparency();
        BufferedImage img = new BufferedImage(w, h, type);
        Graphics2D graphics2d = img.createGraphics();
        graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        graphics2d.rotate(Math.toRadians(degree), (double) w / 2, (double) h / 2);
        graphics2d.drawImage(src, 0, 0, null);
        graphics2d.dispose();
        return img;
    }

    /**
     * 仿射变换
     * <p>
     * ‌‌包括以下常见变换：
     * <p>
     * ‌平移（Translation）‌：将图形在x和y方向上分别移动一定的距离。
     * ‌旋转（Rotation）‌：将图形绕某一点旋转一定的角度。
     * ‌缩放（Scale）‌：在x和y方向上分别按比例放大或缩小图形。
     * ‌剪切（Shear）‌：沿某一方向对图形进行斜向拉伸。
     * ‌反射（Reflection）‌：沿某一轴进行镜像反射‌。
     *
     * @param src 源图像
     * @return BufferedImage
     */
    public static BufferedImage affineTransform(BufferedImage src, AffineTransform affineTransform) {
        int type = src.getColorModel().getTransparency();
        BufferedImage newImg = new BufferedImage(src.getWidth(), src.getHeight(), type);
        Graphics2D g = newImg.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawRenderedImage(src, affineTransform);
        g.dispose();
        return newImg;
    }

    /**
     * 水平翻转仿射变换
     */
    public static AffineTransform matrixVerticalFlip(int height) {
        return new AffineTransform(new double[]{1, 0, 0, -1, 0, height});
    }

    /**
     * 垂直翻转仿射变换
     */
    public static AffineTransform matrixHorizontalFlip(int width) {
        return new AffineTransform(new double[]{-1, 0, 0, 1, width, 0});
    }

    /**
     * 图片添加水印
     *
     * @param src   : 目标文件
     * @param water : 水印文件
     * @param left  : x坐标
     * @param top   : y坐标
     */
    public static BufferedImage pressImage(BufferedImage src, BufferedImage water, int left, int top) {
        int width = src.getWidth(null);
        int height = src.getHeight(null);
        BufferedImage ret = new BufferedImage(width, height, Transparency.TRANSLUCENT);
        Graphics g = ret.createGraphics();
        g.drawImage(src, 0, 0, width, height, null);
        g.drawImage(water, left, top, null, null);
        g.dispose();
        return ret;
    }

    /**
     * 图片添加文字水印
     *
     * @param text : 文字
     * @param left : x坐标
     * @param top  : y坐标
     */
    public static BufferedImage pressText(BufferedImage src, String text, int left, int top) {
        return pressText(src, text, new Font("黑体", Font.PLAIN, 18), Color.BLACK, left, top);
    }

    /**
     * 图片添加文字水印
     *
     * @param text  : 文字
     * @param color : 字体颜色
     * @param left  : x坐标
     * @param top   : y坐标
     * @param font  : 字体
     */
    public static BufferedImage pressText(BufferedImage src, String text, Font font, Color color, int left, int top) {
        int width = src.getWidth(null);
        int height = src.getHeight(null);
        BufferedImage ret = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = ret.createGraphics();
        g.drawImage(src, 0, 0, width, height, null);
        g.setColor(color);
        g.setFont(font);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.drawString(text, left, top);
        g.dispose();
        return ret;
    }

    /**
     * 增加边框
     *
     * @param src : 目标图片
     */
    public static BufferedImage pressBorder(BufferedImage src) {
        int width = src.getWidth(null);
        int height = src.getHeight(null);
        BufferedImage ret = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = ret.createGraphics();
        g.drawImage(src, 0, 0, width, height, null);
        g.setColor(Color.BLACK);
        g.drawRect(0, 0, width - 1, height - 1);
        g.dispose();
        return ret;
    }
}
